﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Trentacular.SharePoint.StsAdmin.Util;

namespace Trentacular.SharePoint.StsAdmin
{
    public class JobDefinitionExecutor : BaseConsoleHelper
    {
        public int JobExecutionTimeoutSeconds { get; set; }

        // Default JobExecutionTimeoutSeconds to 5 minutes
        public JobDefinitionExecutor() : this(300) { }

        public JobDefinitionExecutor(int jobExecutionTimeoutSeconds)
        {
            JobExecutionTimeoutSeconds = jobExecutionTimeoutSeconds;
        }

        public class ExecutionReport
        {
            public ExecutionReport()
            {
                ExecutionCount = 0;
            }

            public int ExecutionCount { get; set; }
            public List<SPJobDefinition> FailedJobs { get; set; }

            public bool HasFailedJobs
            {
                get
                {
                    return FailedJobs != null && FailedJobs.Count > 0;
                }
            }
        }

        /// <summary>
        /// Executes all one-time scheduled job definitions on both the Farm Timer Service and the Web Service
        /// </summary>
        /// <returns>
        /// A list of job definition ids that were aborted or failed during execution
        /// </returns>
        public ExecutionReport ExecuteJobDefinitions()
        {
            var report = new ExecutionReport();
            ExecuteJobDefinitions(report);
            return report;
        }

        private void ExecuteJobDefinitions(ExecutionReport report)
        {
            report.FailedJobs = GetAbortedAndFailedJobs(SPFarm.Local.TimerService);
            report.FailedJobs.AddRange(GetAbortedAndFailedJobs(SPFarm.Local.Services.GetValue<SPWebService>()));

            if (report.HasFailedJobs)
            {
                Console.WriteLine("The following jobs encountered failures:");
                foreach (var failedjob in report.FailedJobs)
                {
                    Console.WriteLine(string.Format("\t{0}", failedjob.Title));
                }

                // Get out of here if there are failures
                return;
            }

            var executionCount = ExecuteJobDefinitions(SPFarm.Local.TimerService);
            executionCount += ExecuteJobDefinitions(SPFarm.Local.Services.GetValue<SPWebService>());

            // Now wait for running jobs to complete
            if (executionCount > 0)
            {
                report.ExecutionCount += executionCount;
                WaitForJobsToComplete(report);
            }
        }

        private List<SPJobDefinition> GetAbortedAndFailedJobs(SPService service)
        {
            return service.RunningJobs.Cast<SPRunningJob>()
                .Where(j => j.Status.Is(SPRunningJobStatus.Aborted, SPRunningJobStatus.Failed) &&
                    j.JobDefinition != null &&
                    IsValidOneTimeScheduledJob(j.JobDefinition)
                    )
                .Select(j => j.JobDefinition)
                .ToList();
        }

        private bool IsValidOneTimeScheduledJob(SPJobDefinition job)
        {
            if (job.IsDisabled || job.Schedule == null)
                return false;

            var schedule = job.Schedule as SPOneTimeSchedule;
            if (schedule == null)
                return false;

            return schedule.Time <= DateTime.Now;
        }

        private int ExecuteJobDefinitions(SPService service)
        {
            var oneTimeJobDefinitions = service.JobDefinitions.Cast<SPJobDefinition>()
                .Where(j => IsValidOneTimeScheduledJob(j))
                .ToList();

            foreach (var job in oneTimeJobDefinitions)
            {
                DoConsoleAction(string.Format("Executing Job Definition: {0}", job.Title), delegate()
                {
                    job.Execute(Guid.Empty);
                });
            }

            return oneTimeJobDefinitions.Count;
        }

        private void WaitForJobsToComplete(ExecutionReport report)
        {
            DoConsoleAction("Waiting for jobs to complete", delegate()
            {
                var stopwatch = Stopwatch.StartNew();

                while (RunningJobsExist)
                {
                    if (stopwatch.Elapsed.Seconds > JobExecutionTimeoutSeconds)
                        throw new TimeoutException(" Job execution timed out");

                    Console.Write(".");
                    Thread.Sleep(1000);
                }

                Console.Write(" ");
            });

            ExecuteJobDefinitions(report);
        }

        private bool RunningJobsExist
        {
            get
            {
                var runningJobs = SPFarm.Local.TimerService.RunningJobs.Cast<SPRunningJob>().ToList();
                runningJobs.AddRange(SPFarm.Local.Services.GetValue<SPWebService>().RunningJobs.Cast<SPRunningJob>());
                return runningJobs.Count(j => j.Status.Is(SPRunningJobStatus.Initialized, SPRunningJobStatus.Scheduled)) > 0;
            }
        }
    }
}
